﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;

namespace TNCGameEngine.Content
{

    /// <summary>
    /// Defines an interface that should be extended to be able to load contents to a particular class.
    /// </summary>
    public interface ILoadable
    {

        /// <summary>
        /// Gets the list of assets to be loaded during the loading process.
        /// </summary>
        /// <returns>A list of AssetToLoad; can be empty, but must NOT be null.</returns>
        List<AssetToLoad> GetAssetsToLoad();

        /// <summary>
        /// Loads the contents that were asked to be loaded (returned by GetAssetsToLoad).
        /// </summary>
        /// <param name="loader">The content loader that the contents were loaded into.</param>
        void LoadContent(ContentLoader loader);

        /// <summary>
        /// Unloads any unloaded contents.
        /// </summary>
        /// <param name="content">The associated content manager.</param>
        void UnloadContent(SharedContentManager content);

    }

    /// <summary>
    /// The content loader, that works in the background helping to load contents asynchronously.
    /// </summary>
    public class ContentLoader
    {

        #region "Properties"

        private LoadingScreen loadingScreen = null;
        public LoadingScreen LoadingScreen
        {
            get { return this.loadingScreen; }
            set { this.loadingScreen = value; }
        }

        volatile private bool stillRunning = false;
        public bool IsLoading
        {
            get { return this.stillRunning; }
        }

        public float Percentage
        {
            get { return (this.maxCount != 0) ? (100.0f * this.count / this.maxCount) : 100.0f; }
        }

        private SpriteFont generalFont = null;
        public SpriteFont GeneralFont
        {
            get { return this.generalFont; }
        }

        public GraphicsDevice GraphicsDevice
        {
            get { return this.game.GraphicsDevice; }
        }

        public SharedContentManager Content
        {
            get { return this.game.Content; }
        }

        #endregion

        #region "Variables"

        volatile private Game game = null;

        volatile private Queue<ReferneceAsset> assets = null;

        volatile private SharedContentManager content = null;

        volatile private int returnstate = GameState.Game;

        volatile private int count = 0;
        volatile private int maxCount = 1;

        private Thread worker = null;


        #endregion

        #region "Constructors"

        public ContentLoader(Game game)
        {

            this.game = game;
            this.content = game.Content;
            
            this.assets = new Queue<ReferneceAsset>();
            
            this.loadingScreen = new LoadingScreen(this);

            this.worker = new Thread(new ThreadStart(Loop));
            this.worker.Name = "Content Loading Thread";
            
        }

        #endregion

        #region "Methods"
        
        public void StartLoading(Game game, int gameState)
        {

            game.CurrentGameState = GameState.Loading;
            this.returnstate = gameState;
            this.stillRunning = true;
            this.count = 0;
            this.maxCount = this.assets.Count;
            this.worker.Start();
            while (!this.worker.IsAlive); // do nothing - wait till the thread starts

        }

        internal void FinishAndContinue(Game game)
        {
            if (this.stillRunning) { return; }
            game.CurrentGameState = this.returnstate;
        }

        private void QueueContent(string path, Type type)
        {
            ReferneceAsset asset = new ReferneceAsset(path);
            asset.Type = type;
            this.assets.Enqueue(asset);
        }

        public void QueueContent<T>(string path)
        {
            this.QueueContent(path, typeof(T));
        }

        public void QueueAllContents(List<AssetToLoad> list)
        {
            foreach (AssetToLoad a in list)
            {
                this.QueueContent(a.Path, a.Type);
            }
        }

        public T GetContent<T>(string path)
        {
            return this.content.GetContent<T>(path);
        }

        private void Loop()
        {

            while (this.assets.Count > 0)
            {
                this.LoadReferenceAsset(this.assets.Dequeue());
                this.count++;
            }

            this.stillRunning = false;

        }

        private void LoadReferenceAsset(ReferneceAsset asset)
        {

            if (asset.Type == typeof(Texture2D))
            {
                asset.Asset = this.LoadTextureStream(game.GraphicsDevice, asset.Path);
            }
            else if (asset.Type == typeof(Model))
            {
                asset.Asset = game.Content.Load<Model>(asset.Path);
            }
            else if (asset.Type == typeof(SpriteFont))
            {
                asset.Asset = game.Content.Load<SpriteFont>(asset.Path);
            }
            else if (asset.Type == typeof(SoundEffect))
            {
                asset.Asset = game.Content.Load<SoundEffect>(asset.Path);
            }
            else if (asset.Type == typeof(Delegate))
            {
                // asset.Asset.Call(game);
            }
            else
            {
                throw new Exception("Unknown load type!");
            }
            
        }

        /// <summary>
        /// Changes the current game state to the return state specified at the start of loading.
        /// </summary>
        internal void ReturnToReturnType()
        {
            this.game.CurrentGameState = this.returnstate;
        }

        /// <summary>
        /// LoadTextureStream method to speed up loading Texture2Ds from pngs,
        /// as described in
        /// http://jakepoz.com/jake_poznanski__speeding_up_xna.html
        /// </summary>
        /// <param name="graphics">Graphics device to use</param>
        /// <param name="loc">Location of the image, root of the path is in the Content folder</param>
        /// <returns>A Texture2D with premultiplied alpha</returns>
        private Texture2D LoadTextureStream(GraphicsDevice graphics, string loc)
        {

            Texture2D file = null;
            RenderTarget2D result = null;

            using (Stream titleStream = TitleContainer.OpenStream(this.content.RootDirectory + "\\" + loc + ".png"))
            {
                file = Texture2D.FromStream(graphics, titleStream);
            }

            //Setup a render target to hold our final texture which will have premulitplied alpha values
            result = new RenderTarget2D(graphics, file.Width, file.Height);

            graphics.SetRenderTarget(result);
            graphics.Clear(Color.Black);

            //Multiply each color by the source alpha, and write in just the color values into the final texture
            BlendState blendColor = new BlendState();
            blendColor.ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue;

            blendColor.AlphaDestinationBlend = Blend.Zero;
            blendColor.ColorDestinationBlend = Blend.Zero;

            blendColor.AlphaSourceBlend = Blend.SourceAlpha;
            blendColor.ColorSourceBlend = Blend.SourceAlpha;

            SpriteBatch spriteBatch = new SpriteBatch(graphics);
            spriteBatch.Begin(SpriteSortMode.Immediate, blendColor);
            spriteBatch.Draw(file, file.Bounds, Color.White);
            spriteBatch.End();

            //Now copy over the alpha values from the PNG source texture to the final one, without multiplying them
            BlendState blendAlpha = new BlendState();
            blendAlpha.ColorWriteChannels = ColorWriteChannels.Alpha;

            blendAlpha.AlphaDestinationBlend = Blend.Zero;
            blendAlpha.ColorDestinationBlend = Blend.Zero;

            blendAlpha.AlphaSourceBlend = Blend.One;
            blendAlpha.ColorSourceBlend = Blend.One;

            spriteBatch.Begin(SpriteSortMode.Immediate, blendAlpha);
            spriteBatch.Draw(file, file.Bounds, Color.White);
            spriteBatch.End();

            //Release the GPU back to drawing to the screen
            graphics.SetRenderTarget(null);

            return result as Texture2D;
        }

        #endregion

        #region "Other Classes"

        /// <summary>
        /// Defines the referenced assets. The Asset field will be non-null only after a successful loading.
        /// </summary>
        internal class ReferneceAsset
        {

            internal ReferneceAsset(string path)
            {
                this.Path = path;
            }
            
            internal string Path = string.Empty;
            internal Type Type = typeof(object);
            internal object Asset = null;

        }

        #endregion

    }

    /// <summary>
    /// Defines a structure to store data to load a batch of assets.
    /// </summary>
    public class AssetToLoad
    {

        public AssetToLoad(string path, Type type)
        {
            this.Path = path;
            this.Type = type;
        }

        internal string Path = string.Empty;
        internal Type Type = typeof(object);

    }

}
